/*
 * SonarLint for IntelliJ IDEA
 * Copyright (C) 2015-2025 SonarSource
 * sonarlint@sonarsource.com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
 */
package org.sonarlint.intellij.ui.vulnerabilities.tree

import com.intellij.ide.DefaultTreeExpander
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.fileEditor.OpenFileDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.ui.PopupHandler
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.EditSourceOnDoubleClickHandler
import com.intellij.util.EditSourceOnEnterKeyHandler
import com.intellij.util.ui.tree.TreeUtil
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeExpansionListener
import javax.swing.tree.TreePath
import org.sonarlint.intellij.actions.MarkAsResolvedAction
import org.sonarlint.intellij.actions.OpenIssueInBrowserAction
import org.sonarlint.intellij.actions.ReopenIssueAction
import org.sonarlint.intellij.actions.SuggestCodeFixIntentionAction
import org.sonarlint.intellij.common.util.SonarLintUtils.getService
import org.sonarlint.intellij.editor.EditorDecorator
import org.sonarlint.intellij.finding.Flow
import org.sonarlint.intellij.finding.FragmentLocation
import org.sonarlint.intellij.finding.SameFileFlowFragment
import org.sonarlint.intellij.finding.issue.vulnerabilities.LocalTaintVulnerability
import org.sonarlint.intellij.telemetry.SonarLintTelemetry
import org.sonarlint.intellij.ui.nodes.FileNode
import org.sonarlint.intellij.ui.tree.TreeCellRenderer
import org.sonarlint.intellij.util.DataKeys.Companion.TAINT_VULNERABILITY_DATA_KEY
import org.sonarlint.intellij.util.openFileFrom

class TaintVulnerabilityTree(private val project: Project, private val model: TaintVulnerabilityTreeUpdater) : Tree(model.model), DataProvider {

    private var selectedTaintVulnerabilityKey: String? = null

    init {
        setShowsRootHandles(false)
        setCellRenderer(TreeCellRenderer(model.renderer))
        expandRow(0)
        selectionModel.addTreeSelectionListener {
            val issueKey = getIssueFromSelectedNode()?.key()
            if (issueKey != null && issueKey != selectedTaintVulnerabilityKey) {
                getService(SonarLintTelemetry::class.java).taintVulnerabilitiesInvestigatedLocally()
            }
            selectedTaintVulnerabilityKey = issueKey
            getSelectedNode()?.let { highlightInEditor(it) }
        }
        addTreeExpansionListener(object : TreeExpansionListener {
            override fun treeExpanded(event: TreeExpansionEvent) {
                expandUniqueFlowNode(event.path)
            }

            override fun treeCollapsed(event: TreeExpansionEvent?) {
                // not interested
            }
        })
        val group = DefaultActionGroup()
        group.add(SuggestCodeFixIntentionAction(getSelectedIssue()))
        group.add(ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_SOURCE))
        group.add(OpenIssueInBrowserAction())
        group.add(MarkAsResolvedAction())
        group.add(ReopenIssueAction())
        group.addSeparator()
        group.add(ActionManager.getInstance().getAction(IdeActions.ACTION_EXPAND_ALL))
        PopupHandler.installPopupMenu(this, group, ActionPlaces.TODO_VIEW_POPUP)
        EditSourceOnDoubleClickHandler.install(this) { showSelectedNodeInEditor() }
        EditSourceOnEnterKeyHandler.install(this) { showSelectedNodeInEditor() }
    }

    private fun expandUniqueFlowNode(treePath: TreePath) {
        val lastNode = treePath.lastPathComponent
        if (lastNode is LocalTaintVulnerability && lastNode.flows.size == 1) {
            expandPath(treePath.pathByAddingChild(lastNode.flows[0]))
        }
    }

    private fun showSelectedNodeInEditor() {
        getSelectedNode()?.let {
            highlightInEditor(it)
            navigateToEditor(it)
        }
    }

    private fun navigateToEditor(node: Any) {
        var rangeMarker: RangeMarker? = null
        if (node is Flow) {
            rangeMarker = node.locations[0].range
        } else if (node is FragmentLocation) {
            rangeMarker = node.location.range
        }
        project.openFileFrom(rangeMarker)
    }

    private fun highlightInEditor(selectedNode: Any?) {
        val parentIssue = getIssueFromSelectedNode()
        val highlighter = getService(project, EditorDecorator::class.java)
        when (selectedNode) {
            is Flow -> highlighter.highlightFlow(selectedNode)
            is FragmentLocation -> highlighter.highlightSecondaryLocation(selectedNode.location, selectedNode.associatedFlow)
            is LocalTaintVulnerability -> highlighter.highlight(selectedNode)
            is SameFileFlowFragment -> parentIssue?.let { highlighter.highlight(it) }
        }
    }

    private fun navigate(): OpenFileDescriptor? {
        val issue = getSelectedIssue() ?: return null
        if (!issue.isValid()) {
            return null
        }
        val file = issue.file() ?: return null
        val offset = issue.rangeMarker()?.startOffset ?: 0
        return OpenFileDescriptor(project, file, offset)
    }

    fun getSelectedIssue(): LocalTaintVulnerability? {
        val node = getSelectedNode()
        return if (node !is LocalTaintVulnerability) {
            null
        } else node
    }

    private fun getSelectedFile(): VirtualFile? {
        val node = getSelectedNode() as? FileNode ?: return null
        return node.file()
    }

    private fun getSelectedNode(): Any? {
        return selectionPath?.lastPathComponent
    }

    override fun getData(dataId: String): Any? {
        return when {
            // use string literal as the key appeared in newer versions
            "bgtDataProvider" == dataId -> getBackgroundDataProvider()
            CommonDataKeys.NAVIGATABLE.`is`(dataId) -> navigate()
            PlatformDataKeys.TREE_EXPANDER.`is`(dataId) -> DefaultTreeExpander(this)
            PlatformDataKeys.VIRTUAL_FILE.`is`(dataId) -> getSelectedFile()
            PlatformDataKeys.VIRTUAL_FILE_ARRAY.`is`(dataId) -> {
                val f = getSelectedFile()
                // return empty so that it doesn't find it in parent components
                if (f != null && f.isValid) arrayOf(f) else arrayOfNulls<VirtualFile>(0)
            }

            TAINT_VULNERABILITY_DATA_KEY.`is`(dataId) -> getSelectedIssue()
            else -> null
        }
    }

    fun setSelectedVulnerability(vulnerability: LocalTaintVulnerability) {
        setAndGetSelectedVulnerability(vulnerability.key())
    }

    fun setAndGetSelectedVulnerability(taintKey: String): LocalTaintVulnerability? {
        return model.taintVulnerabilities.find { it.key() == taintKey }?.let { node ->
            TreeUtil.selectPath(this, TreePath(node))
            return node
        }
    }

    private fun getBackgroundDataProvider(): DataProvider? {
        val file = getSelectedFile()
        return file?.isValid?.let {
            DataProvider { otherId ->
                return@DataProvider if (CommonDataKeys.PSI_FILE.`is`(otherId)) {
                    PsiManager.getInstance(project).findFile(file)
                } else null
            }
        }
    }

    fun getIssueFromSelectedNode(): LocalTaintVulnerability? {
        var currentNodePath = selectionPath
        while (currentNodePath != null) {
            if (currentNodePath.lastPathComponent is LocalTaintVulnerability) {
                return currentNodePath.lastPathComponent as LocalTaintVulnerability
            }
            currentNodePath = currentNodePath.parentPath
        }
        return null
    }

}

